// Jinx AJAX Framework
// Copyright (C) 2008 Blake Hegerle
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

using System;
using System.Collections.Generic;
using System.Linq;
using Jinx.Models.Sequences;

namespace Jinx.Models.Prototyping
{
    public class TypeCheckFailure : Exception
    {
        public TypeCheckFailure(Type Type, string Reason)
            : base(string.Format("Type {0} failed type-checking because {1}",
                                 Type.FullName, Reason))
        {
        }

        public TypeCheckFailure(Type Type, TypeCheckFailure ChainedFailure)
            : base(string.Format("Type {0} failed type-checking", Type.FullName), ChainedFailure)
        {
        }
    }

    public abstract class TypeChecker<T>
    {
        public static void Check()
        {
            try
            {
                CheckType(typeof (T), new Dictionary<Type, Type>());
            }
            catch (TypeCheckFailure f)
            {
                throw new TypeCheckFailure(typeof (T), f);
            }
        }

        private static void CheckType(Type Type, Dictionary<Type, Type> Checked)
        {
            try
            {
                if (Checked.ContainsKey(Type))
                    return;

                Checked[Type] = Type;

                CheckNonPropertyMethods(Type);
                CheckIndexers(Type);
                CheckValidProperties(Type);

                foreach (var ReferencedType in GetReferencedTypes(Type))
                {
                    if (ReferencedType.IsInterface)
                        CheckType(ReferencedType, Checked);
                    else if (ReferencedType.IsClass)
                        CheckThatClassIsStringOrSequence(ReferencedType, Checked);
                    else
                        CheckBuiltInType(ReferencedType);
                }
            }
            catch (TypeCheckFailure f)
            {
                throw new TypeCheckFailure(Type, f);
            }
        }

        private static void CheckThatClassIsStringOrSequence(Type ReferencedType, Dictionary<Type, Type> Checked)
        {
            if (ReferencedType != typeof (string))
                CheckThatClassIsSequence(ReferencedType, Checked);
        }

        private static void CheckThatClassIsSequence(Type Type, Dictionary<Type, Type> Checked)
        {
            if (Checked.ContainsKey(Type))
                return;

            Checked[Type] = Type;

            var TypeArgument = GetTypeArgumentOfGenericType(Type, typeof (Sequence<>));

            var InvalidType = false;
            try
            {
                if (TypeArgument.IsInterface)
                {
                    CheckType(TypeArgument, Checked);
                }
                else if (TypeArgument.IsClass)
                {
                    if (TypeArgument != typeof (string))
                        CheckThatClassIsSequence(TypeArgument, Checked);
                }
                else if (TypeArgument.IsValueType)
                {
                    CheckNullableType(TypeArgument);
                }
                else
                {
                    InvalidType = true;
                }
            }
            catch (TypeCheckFailure f)
            {
                throw new TypeCheckFailure(Type, f);
            }

            if (InvalidType)
                throw new TypeCheckFailure(TypeArgument, "It is not allowed as a type argument to Sequence<>");
        }

        private static void CheckNullableType(Type Type)
        {
            Type TypeArgument;

            try
            {
                TypeArgument = GetTypeArgumentOfGenericType(Type, typeof (Nullable<>));
            }
            catch (TypeCheckFailure f)
            {
                throw new TypeCheckFailure(Type, f);
            }

            if (TypeArgument.IsPrimitive == false)
                throw new TypeCheckFailure(Type, "Type argument to Nullable<> must be primitive");
        }

        private static Type GetTypeArgumentOfGenericType(Type GenericType, Type ExpectedDeclaringType)
        {
            var GenericArguments = GenericType.GetGenericArguments();

            if (GenericArguments.Length != 1)
                throw new TypeCheckFailure(GenericType, "It is not a generic type with one type-argument");

            if (GenericType.GetGenericTypeDefinition() != ExpectedDeclaringType)
                throw new TypeCheckFailure(GenericType, "It is not a " + ExpectedDeclaringType.Name);

            return GenericArguments[0];
        }

        private static void CheckValidProperties(Type Type)
        {
            if ((from Property in PropertyEnumerator.Properties(Type)
                 where (Property.CanRead == false) || (Property.CanWrite == false)
                 select Property).Count() > 0)
            {
                throw new TypeCheckFailure(Type, "all properties must be read-write");
            }
        }

        private static void CheckIndexers(Type Type)
        {
            var NumIndexers = (from Property in PropertyEnumerator.Properties(Type)
                               where Property.GetIndexParameters().Length > 0
                               select Property).Count();

            if (NumIndexers > 0)
                throw new TypeCheckFailure(Type, "it has an indexer");
        }

        private static void CheckBuiltInType(Type ReferencedType)
        {
            if (ReferencedType.IsPrimitive)
                return;

            if (ReferencedType.IsArray)
                throw new TypeCheckFailure(ReferencedType, "it is an array");

            if (ReferencedType.IsClass)
                throw new TypeCheckFailure(ReferencedType, "it is a class");

            if (ReferencedType.IsEnum)
                throw new TypeCheckFailure(ReferencedType, "it is an enumeration");

            if (ReferencedType.IsValueType)
                throw new TypeCheckFailure(ReferencedType, "it is a value type");
        }

        private static IEnumerable<Type> GetReferencedTypes(Type Type)
        {
            return (from Property in PropertyEnumerator.Properties(Type)
                    select Property.PropertyType).Distinct();
        }

        private static void CheckNonPropertyMethods(Type Type)
        {
            var Gets = from Prop in PropertyEnumerator.Properties(Type)
                       select Prop.GetGetMethod();
            var Sets = from Prop in PropertyEnumerator.Properties(Type)
                       select Prop.GetSetMethod();
            var Methods = Type.GetMethods().Except(Gets).Except(Sets);

            if (Methods.Count() > 0)
                throw new TypeCheckFailure(Type, "it has methods");
        }
    }
}